{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "[find_floating_potential()]: ../../../api/plasmapy.analysis.swept_langmuir.floating_potential.find_floating_potential.rst#find-floating-potential\n",
    "\n",
    "# Swept Langmuir Analysis: Floating Potential\n",
    "\n",
    "This notebook covers the use of the [find_floating_potential()] function and how it is used to determine the floating potential from a swept Langmuir trace.\n",
    "\n",
    "The floating potential, $V_f$, is defined as the probe bias voltage at which there is no net collected current, $I=0$.  This occurs because the floating potential slows the collected electrons and accelerates the collected ions to a point where the electron- and ion-currents balance each other out."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%matplotlib inline\n",
    "\n",
    "from pathlib import Path\n",
    "\n",
    "import matplotlib.pyplot as plt\n",
    "import numpy as np\n",
    "\n",
    "from plasmapy.analysis import swept_langmuir as sla\n",
    "\n",
    "plt.rcParams[\"figure.figsize\"] = [10.5, 0.56 * 10.5]\n",
    "\n",
    "np.set_printoptions(precision=4, threshold=16)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Contents:\n",
    "\n",
    "1. [How find_floating_potential() works](#How-find_floating_potential()-works)\n",
    "    1. [Notes about usage](#Notes-about-usage)\n",
    "    1. [Knobs to turn](#Knobs-to-turn)\n",
    "1. [Calculate the Floating Potential](#Calculate-the-Floating-Potential)\n",
    "    1. [Interpreting results](#Interpreting-results)\n",
    "    1. [Plotting results](#Plotting-results)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## How `find_floating_potential()` works\n",
    "\n",
    "1. The passed current array is scanned for points that equal zero and point-pairs that straddle where the current, $I$, equals zero.  This forms an collection of \"crossing-points.\"\n",
    "1. The crossing-points are then grouped into \"crossing-islands\" based on the `threshold` keyword.\n",
    "    - A new island is formed when a successive crossing-point is more (index) steps away from the previous crossing-point than defined by `threshold`.  For example, if `threshold=4` then a new island is formed if a crossing-point candidate is more than 4 steps away from the previous candidate.\n",
    "    - If multiple crossing-islands are identified, then the function will compare the total span of all crossing-islands to `min_points`.  If the span is greater than `min_points`, then the function is incapable of identifying $V_f$ and will return `numpy.nan` values; otherwise, the span will form one larger crossing-island.\n",
    "1. To calculate the floating potential...\n",
    "    - If the number of points that make up the crossing-island is less than `min_points`, then each side of the \"crossing-island\" is equally padded with the nearest neighbor points until `min_points` is satisfied.\n",
    "    - If `fit_type=\"linear\"`, then a `scipy.stats.linregress` fit is applied to the points that make up the crossing-island.\n",
    "    - If `fit_type=\"exponential\"`, then a `scipy.optimize.curve_fit` fit is applied to the points that make up the crossing-island."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Notes about usage\n",
    "\n",
    "- The function provides no signal processing.  If needed, the user must smooth, sort, crop, or process the arrays before passing them to the function.\n",
    "- The function requires the voltage array to be monotonically increasing.\n",
    "- If the total range spanned by all crossing-islands is less than or equal to `min_points`, then `threshold` is ignored and all crossing-islands are grouped into one island."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Knobs to turn\n",
    "\n",
    "- `fit_type`\n",
    "\n",
    "    There are two types of curves that can be fitted to the identified crossing point data: `\"linear\"` and `\"exponential\"`.  The former will fit a line to the data, whereas, the latter will fit an exponential curve with an offset.  The default curve is `\"exponential\"` since swept Langmuir data is not typically linear as it passes through $I=0$.\n",
    "\n",
    "- `min_points`\n",
    "\n",
    "    This variable specifies the minimum number of points that will be used in the curve fitting.  As mentioned above, the crossing-islands are identified and then padded until `min_points` is satisfied.  Usage:\n",
    "    \n",
    "    - `min_points = None` (DEFAULT):  `min_points` is chosen to be the larger of 5 or `factor * array_size`, where `factor = 0.1` for `\"linear\"` and `0.2` for `\"exponential\"`.\n",
    "    - `min_points = numpy.inf`:  The entire array is fitted.\n",
    "    - `min_points` is an integer `>= 1`:  `min_points` is the minimum number of points to be used.\n",
    "    - `0 < min_points < 1`:  The minimum number of points is taken as `min_points * array_size`.\n",
    "\n",
    "- `threshold`\n",
    "\n",
    "    The max allowed index distance between crossing-points before a new crossing-island is formed."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Calculate the Floating Potential\n",
    "\n",
    "Below we'll compute the floating potential using the default fitting behavior (`fit_type=\"exponential\"`) and a linear fit (`fit_type=\"linear\"`)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# load data\n",
    "filename = \"Beckers2017_noisy.npy\"\n",
    "filepath = (Path.cwd() / \"..\" / \"..\" / \"langmuir_samples\" / filename).resolve()\n",
    "voltage, current = np.load(filepath)\n",
    "\n",
    "# voltage array needs to be monotonically increasing/decreasing\n",
    "isort = np.argsort(voltage)\n",
    "voltage = voltage[isort]\n",
    "current = current[isort]\n",
    "\n",
    "# get default fit results (exponential fit)\n",
    "results = sla.find_floating_potential(voltage, current, min_points=0.3)\n",
    "\n",
    "# get linear fit results\n",
    "results_lin = sla.find_floating_potential(voltage, current, fit_type=\"linear\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "[find_floating_potential()]: ../../../api/plasmapy.analysis.swept_langmuir.floating_potential.find_floating_potential.rst#find-floating-potential\n",
    "[VFExtras]: ../../../api/plasmapy.analysis.swept_langmuir.floating_potential.VFExtras.rst#vfextras\n",
    "\n",
    "### Interpreting results\n",
    "\n",
    "The [find_floating_potential()] function returns a 2 element tuple, where the first element is the calculated floating potential $V_f$ and the second element is a named tuple [VFExtras] containing additional parameters resulting from the calculation.\n",
    "\n",
    "- `results[0]` is the determined floating potential (same units as the pass `voltage` array)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "vf = results[0]\n",
    "vf"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "[VFExtras]: ../../../api/plasmapy.analysis.swept_langmuir.floating_potential.VFExtras.rst#vfextras\n",
    "\n",
    "- `results[1]` is an instance of [VFExtras] and contains additional information from the calculation"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "extras = results[1]\n",
    "extras"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- `extras[0]` = `extras.vf_err` = the associated uncertainty in the $V_f$ calculation (same units as `vf`)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "(extras[0], extras.vf_err)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- `extras[1]` = `extras.rsq` = the coefficient of determination (r-squared) value of the fit"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "(extras[1], extras.rsq)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "[FitFunction]: ../../../api_static/plasmapy.analysis.fit_functions.rst\n",
    "\n",
    "- `extras[2]` = `extras.fitted_func` = the resulting fitted function\n",
    "\n",
    "    - `extras.fitted_func` is a callable representation of the fitted function `I = extras.fitted_func(V)`.\n",
    "    - `extras.fitted_func` is an instance of a sub-class of `AbstractFitFunction`. ([FitFunction classes][FitFunction])\n",
    "    - Since `extras.fitted_func` is a class instance, there are many other attributes available.  For example,\n",
    "        - `extras.fitted_func.params` is a named tuple of the fitted parameters\n",
    "        - `extras.fitted_func.param_errors` is a named tuple of the fitted parameter errors\n",
    "        - `extras.fitted_func.root_solve()` finds the roots of the fitted function. This is how $V_f$ is calculated."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "(\n",
    "    extras[2],\n",
    "    extras.fitted_func,\n",
    "    extras.fitted_func.params,\n",
    "    extras.fitted_func.params.a,\n",
    "    extras.fitted_func.param_errors,\n",
    "    extras.fitted_func.param_errors.a,\n",
    "    extras.fitted_func(vf),\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- `extras[3]` = `extras.islands` = a list of slice objects representing all the identified crossing-islands"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "(\n",
    "    extras[3],\n",
    "    extras.islands,\n",
    "    voltage[extras.islands[0]],\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- `extras[4]` = `extras.fitted_indices` = a slice object representing the indices used in the fit"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "(\n",
    "    extras[4],\n",
    "    extras.fitted_indices,\n",
    "    voltage[extras.fitted_indices],\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Plotting results"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "nbsphinx-thumbnail": {
     "tooltip": "Floating Potential"
    }
   },
   "outputs": [],
   "source": [
    "figwidth, figheight = plt.rcParams[\"figure.figsize\"]\n",
    "figheight = 2.0 * figheight\n",
    "fig, axs = plt.subplots(3, 1, figsize=[figwidth, figheight])\n",
    "\n",
    "# plot original data\n",
    "axs[0].set_xlabel(\"Bias Voltage (V)\", fontsize=12)\n",
    "axs[0].set_ylabel(\"Current (A)\", fontsize=12)\n",
    "\n",
    "axs[0].plot(voltage, current, zorder=10, label=\"Sweep Data\")\n",
    "axs[0].axhline(0.0, color=\"r\", linestyle=\"--\", label=\"I = 0\")\n",
    "axs[0].legend(fontsize=12)\n",
    "\n",
    "# zoom on fit\n",
    "for ii, label, rtn in zip(\n",
    "    [1, 2], [\"Exponential\", \"Linear\"], [results, results_lin], strict=False\n",
    "):\n",
    "    vf = rtn[0]\n",
    "    extras = rtn[1]\n",
    "\n",
    "    # calc island points\n",
    "    isl_pts = np.array([], dtype=np.int64)\n",
    "    for isl in extras.islands:\n",
    "        isl_pts = np.concatenate((isl_pts, np.r_[isl]))\n",
    "\n",
    "    # calc xrange for plot\n",
    "    xlim = [voltage[extras.fitted_indices].min(), voltage[extras.fitted_indices].max()]\n",
    "    vpad = 0.25 * (xlim[1] - xlim[0])\n",
    "    xlim = [xlim[0] - vpad, xlim[1] + vpad]\n",
    "\n",
    "    # calc data points for fit curve\n",
    "    mask1 = np.where(voltage >= xlim[0], True, False)\n",
    "    mask2 = np.where(voltage <= xlim[1], True, False)\n",
    "    mask = np.logical_and(mask1, mask2)\n",
    "    vfit = np.linspace(xlim[0], xlim[1], 201, endpoint=True)\n",
    "    ifit, ifit_err = extras.fitted_func(vfit, reterr=True)\n",
    "\n",
    "    axs[ii].set_xlabel(\"Bias Voltage (V)\", fontsize=12)\n",
    "    axs[ii].set_ylabel(\"Current (A)\", fontsize=12)\n",
    "    axs[ii].set_xlim(xlim)\n",
    "\n",
    "    axs[ii].plot(\n",
    "        voltage[mask],\n",
    "        current[mask],\n",
    "        marker=\"o\",\n",
    "        zorder=10,\n",
    "        label=\"Sweep Data\",\n",
    "    )\n",
    "    axs[ii].scatter(\n",
    "        voltage[extras.fitted_indices],\n",
    "        current[extras.fitted_indices],\n",
    "        linewidth=2,\n",
    "        s=6**2,\n",
    "        facecolors=\"deepskyblue\",\n",
    "        edgecolors=\"deepskyblue\",\n",
    "        zorder=11,\n",
    "        label=\"Points for Fit\",\n",
    "    )\n",
    "    axs[ii].scatter(\n",
    "        voltage[isl_pts],\n",
    "        current[isl_pts],\n",
    "        linewidth=2,\n",
    "        s=8**2,\n",
    "        facecolors=\"deepskyblue\",\n",
    "        edgecolors=\"black\",\n",
    "        zorder=12,\n",
    "        label=\"Island Points\",\n",
    "    )\n",
    "    axs[ii].autoscale(False)\n",
    "    axs[ii].plot(vfit, ifit, color=\"orange\", zorder=13, label=label + \" Fit\")\n",
    "    axs[ii].fill_between(\n",
    "        vfit,\n",
    "        ifit + ifit_err,\n",
    "        ifit - ifit_err,\n",
    "        color=\"orange\",\n",
    "        alpha=0.12,\n",
    "        zorder=0,\n",
    "        label=\"Fit Error\",\n",
    "    )\n",
    "    axs[ii].axhline(0.0, color=\"r\", linestyle=\"--\")\n",
    "    axs[ii].fill_between(\n",
    "        [vf - extras.vf_err, vf + extras.vf_err],\n",
    "        axs[1].get_ylim()[0],\n",
    "        axs[1].get_ylim()[1],\n",
    "        color=\"grey\",\n",
    "        alpha=0.1,\n",
    "    )\n",
    "    axs[ii].axvline(vf, color=\"grey\")\n",
    "    axs[ii].legend(fontsize=12)\n",
    "\n",
    "    # add text\n",
    "    rsq = extras.rsq\n",
    "    txt = f\"$V_f = {vf:.2f} \\\\pm {extras.vf_err:.2f}$ V\\n\"\n",
    "    txt += f\"$r^2 = {rsq:.3f}$\"\n",
    "    txt_loc = [vf, axs[ii].get_ylim()[1]]\n",
    "    txt_loc = axs[ii].transData.transform(txt_loc)\n",
    "    txt_loc = axs[ii].transAxes.inverted().transform(txt_loc)\n",
    "    txt_loc[0] -= 0.02\n",
    "    txt_loc[1] -= 0.26\n",
    "    axs[ii].text(\n",
    "        txt_loc[0],\n",
    "        txt_loc[1],\n",
    "        txt,\n",
    "        fontsize=\"large\",\n",
    "        transform=axs[ii].transAxes,\n",
    "        ha=\"right\",\n",
    "    )"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
